04. 불변 활용하기: 안정적으로 동작하게 만들기
📌 Contents
📌 재할당
- 변수에 값을 다시 할당하는 것을
재할당
또는파괴적 할당
이라고 함 - 재할당은 변수의 의미를 바꿔 추측하기 어렵게 만들고, 언제 어떻게 변경되었는지 추적하기 힘들게 함
- final 수식자를 붙여 불변 변수를 만들어서 재할당을 막을 수 있음 (Java)
- 매개 변수도 final을 이용하여 불변변수로 만들어야 함
📌 가변으로 인해 발생하는 의도하지 않은 영향
- 인스턴스가 가변이면 다른 부분에 의도하지 않은 영향을 주기 쉬움
- 의도하지 않게 영향을 끼치는 경우 두 가지와 문제를 개선해 주는 설계 방법에 대해 설명할 것임
사례 1: 가변 인스턴스 재사용하기
- 가변 인스턴스를 재사용하면 한쪽의 변경이 다른 한 쪽에 영향을 줄 수 있음
AttackPower 클래스
class AttackPower { static final int MIN = 0; int value; // final을 붙이지 않았으므로 가변 AttackPower(int value) { if (value < MIN) { throw new IllegalArgumentException(); } this.value = value; } }
Weapon 클래스
class Weapon { final AttackPower attackPower; Weapon(AttackPower attackPower) { this.attackPower = attackPower; } }
AttackPower 인스턴스를 재사용 하는 경우
AttackPower attackPower = new AttackPower(20); Weapon weaponA = new Weapon(attackPower); Weapon weaponB = new Weapon(attackPower); weaponA.attackPower.value = 25; System.out.println("Weapon A attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 25 System.out.println("Weapon B attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 25
AttackPower 인스턴스를 개별적으로 생성 하는 경우
AttackPower attackPowerA = new AttackPower(20); AttackPower attackPowerB = new AttackPower(20); Weapon weaponA = new Weapon(attackPowerA); Weapon weaponB = new Weapon(attackPowerB); weaponA.attackPower.value = 25; System.out.println("Weapon A attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 25 System.out.println("Weapon B attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 20
사례 2: 함수로 가변 인스턴스 조작하기
- 예상치 못한 동작은 함수(메서드) 떄문에 발생하기도 함
AttackPower 클래스에 공격력을 변화시키는 reinforce 메서드와 disable 메서드 추가
class AttackPower { static final int MIN = 0; int value; // final을 붙이지 않았으므로 가변 AttackPower(int value) { if (value < MIN) { throw new IllegalArgumentException(); } this.value = value; } /** * 공격력 강화하기 * @param increment 공격력 증가 */ void reinforce(int increment) { value += increment; } /** 무력화하기 */ void disable() { value = MIN; } }
이런 구조에서 reinforce가 잘 동작할 것임
- 하지만 갑자기 공격력이 0이 되는 일이 종종 발생할 수 있음
- AttackPower 인스턴스가 다른 스레드에서 실행하면서, AttackPower.disable 메서드를 호출했기 때문임
- Attacker의 disable 메서드와 reinforce 메서드는 부수 효과 문제를 갖고 있음
부수 효과의 단점
- 함수의 부수 효과는 '함수가 매개변수를 전달받고, 값을 리턴하는 것' 이외에 외부 상태(인스턴스 변수 등)를 변경하는 것을 가리킴
- 함수(메서드)에는 주요 작용과 부수 효과가 있음
- 주요 작용: 함수(메서드)가 매개변수를 전달받고, 값을 리턴하는 것
- 부수 효과: 주요 작용 이외의 상태 변경을 일으키는 것
- 인스턴스 변수 변경
- 전역 변수 변경
- 매개변수 변경
- 파일 읽고 쓰기 같은 I/O 조작
- 부수 효과를 일으키는 코드는 결과를 예측하기 힘들며, 유지 보수하기 힘듬
- 함수 내부에 선언한 지역 변수의 변경은 함수 외부에 영향을 주지 않기 때문에 부수 효과가 아님
함수의 영향 범위 한정하기
- 부수 효과가 있는 함수는 영향 범위를 예측하기 어렵기 때문에, 함수가 영향을 주거나 받을 수 있는 범위를 한정하는 것이 좋음
- 함수는 다음 항목을 만족하도록 설계하는 것이 좋음
- 데이터(상태)는 매개변수로 받기
- 상태를 변경하지 않기
- 값을 함수의 리턴 값으로 돌려주기
- 즉, 매개변수로 상태를 받고, 상태를 변경하지 않고, 값을 리턴하기만 하는 함수가 이상적임
- 그러면 메서드에서 인스턴스 변수를 사용하는 것도 안 좋을까?
- 인스턴스 변수는 불변으로 만들어 영향이 전달되지 않게 할 수 있으므로, 예상치 못한 동작 문제를 회피할 수 있음
- 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적임
불변으로 만들어서 예기치 못한 동작 막기
- AttackPower 클래스의 인스턴스 변수 value가 가변적이므로, 부수 효과가 발생할 여지가 있었음
- '주의해서 코드를 작성할 테니 불변이 아니여도 괜찮을 거야'라는 생각은 스스로를 너무 맹신하는 것
- 문제를 일으킬 가능성은 항상 존재하고, 코드가 많을수록 가능성은 더욱 커짐
- 따라서 부수 효과의 여지 자체를 없애기 위해 인스턴스 변수를 불변으로 만들어야 함
개선된 AttackPower 클래스
: value를 불변 변수로 만들고, reinforce와 disable 메서드에서 AttackPower 인스턴스를 새로 생성해 리턴하기class AttackPower { static final int MIN = 0; int value; // final을 붙이지 않았으므로 가변 AttackPower(int value) { if (value < MIN) { throw new IllegalArgumentException(); } this.value = value; } /** * 공격력 강화하기 * @param increment 공격력 증가 * @return 증가된 공격력 */ AttackPower reinforce(final AttackPower increment) { return new AttackPower(this.value + increment.value); } /** * 무력화하기 * @return 무력화한 공격력 */ AttackPower disable() { return new AttackPower(MIN); } }
개선된 Weapon 클래스
class Weapon { final AttackPower attackPower; Weapon(AttackPower attackPower) { this.attackPower = attackPower; } /** * 무기 강화하기 * @param increment 공격력 강화 * @return 강화된 무기 */ Weapon reinforce(final AttackPower increment) { final AttackPower reinforced = attackPower.reinforce(increment); return new Weapon(reinforced); } }
개선된 AttackPower와 Weapon 사용 버전
AttackPower attackPowerA = new AttackPower(20); AttackPower attackPowerB = new AttackPower(20); Weapon weaponA = new Weapon(attackPowerA); Weapon weaponB = new Weapon(attackPowerB); final AttackPower increment = new AttackPower(5); final Weapon reinforcedWeaponA = weaponA.reinforce(increment); System.out.println("Weapon A attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 20 System.out.println("Reinforced weapon A attack power: " + reinforcedWeaponA.attackPower.value); // output: Reinforced weapon A attack power: 25 System.out.println("Weapon B attack power: " + weaponA.attackPower.value); // output: Weapon A attack power: 20
📌 불변과 가변은 어떻게 다루어야 할까
기본적으로 불변으로
- 변수를 불변으로 만들었을 때의 장점
- 변수의 의미가 변하지 않으므로, 혼란을 줄일 수 있음
- 동작이 안정적이게 되므로, 결과를 예측하기 쉬움
- 코드의 영향 범위가 한정적이므로, 유지 보수가 편리해짐
가변으로 설계해야 하는 경우
- 성능이 중요한 경우 가변이 필요함
- 대량의 데이터를 빠르게 처리해야 하는 경우
- 이미지를 처리하는 경우
- 리소스에 제약이 큰 임베디드 소프트웨어를 다루는 경우
- 불변이라면 값을 변경할 때마다 인스턴스를 새로 생성하는데, 크기가 큰 인스턴스를 새로 생성하면서 시간이 오래걸려 성능에 문제가 생긴다면, 불변보다는 가변을 사용하는 것이 좋음
- 반복문 카운터 등 스코프가 국소적인 경우에 스코프에서만 사용되는 지역 변수는 가변으로 해도 괜찮음
코드 외부와 데이터 교환은 국소화하기
- 파일을 읽고 쓰는 I/O 조작은 코드 외부의 상태에 의존하고, 웹 애플리케이션도 거의 필수로 데이터베이스를 사용함
- 코드를 아무리 주의 깊게 작성해도, 파일이나 데이터베이스는 코드 외부에 있는 상태임
- 파일의 내용은 다른 시스템에 의해서 덮어 쓰일 수 있음
- 코드 내부에서는 이러한 외부 동작을 제어할 수 없음
- 최근에는 이러한 영향을 그나마 줄일 수 있게 코드 외부와 데이터 교환을 국소화하는 테크닉을 많이 사용함
- 예를 들어 데이터베이스의 영속화(데이터베이스에 데이터를 저장하는 것)를 캡슐화 하는 디자인 패턴인 리포지터리 패턴이 있음
❓ Questions
❓ 증가하는 값(increment)도 새로운 객체를 만드는 것은 가변을 불변으로 바꾸는 것과 관련이 있는 것인가?
- 책의 예제 코드에서 무기에 공격력을 올릴 때 증가하는 값을 int가 아닌 AttackPower로 준다.
- 그런데 어차피 증가하는 값은 다른 인스턴스에 영향을 안주기 때문에 불변이나 부수효과랑은 관련이 없는 것 같다.
- 그럼 왜 increment도 기본 int 형이 아니라 AttackPower로 선언해서 넘겨준 것일까?
- 이 이유는 앞 장에서 설명한 잘못된 값 할당과 관련이 있는 것 같다.
- int 형이면 음수도 들어갈 수 있는데 증가 값이 음수면 안되기 때문에 int 형이 아닌 AttackPower로 선언 한 것 같다.
- AttakcPower는 value가 음수가 되지 못하도록 가드가 되어있다.
❓ 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적이다?
- 메서드에서 인스턴스 변수를 쓰는 것은 인스턴스 변수를 불변으로 만들어 예기치 못한 동작을 막을 수 있어서 괜찮다고 한다.
- 또한 객체 지향 프로그래밍 언어는 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용하는 것이 일반적임
- 부수 효과로 인한 범위를 클래스 내부를 허용한다는 것이 무슨 뜻일까?
- 클래스 내부의 메서드끼리는 부수효과가 일어나도 괜찮다는 것인가?
- 예제를 보면 reinforce와 disable 메서드가 같은 인스턴스 변수 value를 변경시키서 서로 부수 효과가 일어나서 문제라고 하고 있는 것 아닌가?
- 계속 생각을 해보아도 클래스 내부에서 부수효과가 일어나는 것을 막기 위해 인스턴스 변수를 사용할 때 불변으로 만들어 사용하는 것 같은데, 함수의 부수 효과로 인한 범위를 클래스 내부까지 허용한다는 말이 잘 이해가 되지 않는다.
- 일단은 메서드에서 인스턴스 변수를 사용하는 것은 불변을 이용하여 예기치 못한 동작을 막을 수 있어 괜찮다 정도만 알고 넘어가야 될 것 같다.
❓ mvc 패턴과 리포지토리 패턴
- mvc 패턴이란 model + view + controller 패턴이다.
- model은 데이터 쿼리 로직과 비즈니스 로직을 다룬다
- view는 사용자에게 보여지는 부분을 다룬다
- controller는 view와 model을 연결해주는 역할을 한다.
- view와 model은 서로 알지 못하고 controller를 통해서만 통신을 한다.
- 여기서 model은 데이터 쿼리 로직과 비즈니스 로직을 다룬다고 했는데, 보통 mvc 패턴에서는 service가 있다고 배웠다.
- model(보통 repository라고 부름)에서는 쿼리를 이용해 데이터베이스에 접근하는 로직만 있다.
- 불러온 데이터를 받아 가공 등을 하는 비즈니스 로직은 다 service에서 처리를 한다.
- 리포지토리 패턴은 데이터베이스 관련 로직과 애플리케이션 로직을 분리하는 패턴이라고 책에 나온다.
- 책을 읽고 mvc 패턴과 리포지토리 패턴이 크게 관련이 되있다고 생각이 들었다.
- mvc 패턴에서 쿼리를 이용해 데이터를 가져오는 repository와 그 데이터를 처리하는 service로 나눈 것이 리포지토리 패턴을 적용한 것인가?
- 아니면 데이터를 가져와 처리하는 M과 그 데이터를 보여주는 V를 나눈 것이 리포지토리 패턴을 적용한 것인가?
- mvc 패턴은 데이터를 다루는 model, 보이는 쪽을 다루는 view 그 사이에 controller가 있는 패턴이다.
- model 부분에서 반드시 데이터 쿼리 로직(repository)과 비즈니스 로직(service)을 나누는 것은 아닌 것 같다.
- 하지만 spring 같은 프레임 워크에서는 mvc 패턴에서 repository와 service를 분리한다.
- 이런 경우 mvc 패턴과 리포지토리 패턴을 모두 적용한 경우인 것 같다.